package club.jdiy.utils.excel;

import club.jdiy.core.ex.JDiyException;
import club.jdiy.utils.StringUtils;
import club.jdiy.utils.poi.ImportMeta;
import club.jdiy.utils.poi.Save;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * JDiy新版Excel数据导入工具类.
 *
 * <p>示例代码参考：
 * <br/> 假设有一个Student学生实体 <br/>
 * 　　实体有字段：serialNo:学号 name:姓名  banji:班级；<br/>
 * 　　excel文件中有这三例数据（学号，姓名，班级)<br/>
 * <pre>
 *
 * Map<HeadParser<Student>> im = new HashMap<>();　//定义excel表头, JDiy将按表头进行数据读取并执行导入。
 *
 * //定义学号列如何导入：
 * //map的key对应表头内容，可以是实体字段名也可以是中文表头标题．
 * //map是该表头所在列的导入字段处理函数(HeaderParser)，此处简写为lambda表达式,两个参数: o为Student学生实体对象;value是从Excel单元格中取到的值（HeaderParser就是要告诉程序，如何把这一列的数据存入）：
 *  im.put("学号", (o, value) -> {
 *      if (value == null || "".equals(value.trim())) throw new JDiyException("学号不能为空");
 *      o.setSerialNo(value);
 *  });
 *
 *  im.put("姓名", (o, value) -> {
 *      if (value == null || "".equals(value.trim())) throw new JDiyException("姓名不能为空");
 *      o.setName(value);
 *  });
 *  im.put("班级", Student::setBanji); //无需其它业务逻辑直接存入字段的更简洁的lambda
 * </pre>
 * </p>
 *
 * @param <T> 实体类型
 * @author ziquee(子秋) QQ号:39886616 主页: http://www.jdiy.club
 * @version 20210603
 */
public class ExcelImport<T> {
    private final Class<T> tClass;

    /**
     * 构建数据导入处理．
     *
     * @param tClass 持久化实体数据模型类．
     */
    public ExcelImport(Class<T> tClass) {
        this.tClass = tClass;
    }

    /**
     * 执行Excel数据导入.<br/>
     *
     * @param heads       预先定义要导入的列数据转换函数,它是一个Map集合，Key为表头， value为该列的数据处理函数.
     * @param file1       要导入的Excel文件(xls, xlsx格式均可)
     * @param saveHandler 定义并传入一个持久化处理函数，告诉导入程序，如何最终保存数据．
     * @return 返回成功导入的数据条数.
     * @throws JDiyException 若导入异常，则抛出.
     */
    public int doImport(Map<String, HeadParser<T>> heads, MultipartFile file1, Save<T> saveHandler) throws JDiyException {
        return doImport(heads, 1, file1, saveHandler);
    }

    /**
     * 执行Excel数据导入.<br/>
     *
     * @param heads       预先定义要导入的列数据转换函数,它是一个Map集合，Key为表头， value为该列的数据处理函数.
     * @param headRowNum  提示表头所在的行，从该行位置开始检查表头，并从它的下一行开始执行数据的导入。
     * @param file1       要导入的Excel文件(xls, xlsx格式均可)
     * @param saveHandler 定义并传入一个持久化处理函数，告诉导入程序，如何保存数据．
     * @return 返回成功导入的数据条数.
     * @throws JDiyException 若导入异常，则抛出.
     */
    public int doImport(Map<String, HeadParser<T>> heads, int headRowNum, MultipartFile file1, Save<T> saveHandler) throws JDiyException {
        if (!file1.isEmpty()) {
            Workbook book;
            try {
                if (file1.getOriginalFilename() != null && file1.getOriginalFilename().toLowerCase().endsWith(".xlsx")) {
                    book = new XSSFWorkbook(file1.getInputStream());
                } else {
                    book = new HSSFWorkbook(file1.getInputStream());
                }
            } catch (IOException e) {
                throw new JDiyException("无法读取Excel文件，请检查上传文件的有效性." + e.getMessage());
            }
            Sheet sheet = book.getSheetAt(0);

            StringBuilder errSB = new StringBuilder();
            int okRowsCount = 0;
            int breakNummRowCount = 0;//有时候excel编辑后，删除了一些行后保存导入，会出现提示很大的行号为null报错导致导入失败的bug,加这个参数，判断中间连续空行超过50行时停止导入
            int startRow = headRowNum - 1;
            //由于新版导入程序不再限定列的前后顺序(即用户导入时表头列的前后顺序可以打乱)
            //通过读取实际的表头行来确定目标列到底是什么字段，这个parserMap的key是列下标，value就是那个headParser.
            // 这样在读取数据行的每列数据的时候(列下标已知),就能通过此map确定目标列的处理函数。
            Map<Short, HeadParser<T>> parserMap = new HashMap<>();
            for (int rowNum = startRow; rowNum <= sheet.getLastRowNum(); rowNum++) {
                AtomicBoolean hasErr = new AtomicBoolean(false);
                StringBuilder err = new StringBuilder("行" + (rowNum + 1) + ": ");

                Row row = sheet.getRow(rowNum);
                if (row == null) {
                    breakNummRowCount++;
                    if (breakNummRowCount > 50) break;
                    continue;
                } else {
                    breakNummRowCount = 0;
                }
                if (rowNum == startRow) {
                    for (short colNum = 0; colNum < row.getLastCellNum(); colNum++) {
                        Cell cell = row.getCell(colNum);
                        String rn = getValue(cell);
                        if (heads.containsKey(rn)) {
                            parserMap.put(colNum, heads.get(rn));
                        } else {
                            throw new JDiyException("无法识别的列[{}],请检查表头是否与模板一致，若该列不需要导入，请将其删除", rn);
                        }
                    }
                    continue;
                }

                T vo;
                try {
                    vo = tClass.getDeclaredConstructor().newInstance();
                } catch (Exception e) {
                    JDiyException le = new JDiyException(e.getMessage());
                    le.setStackTrace(e.getStackTrace());
                    throw le;
                }
                parserMap.forEach((i, p) -> {
                    Cell cell = row.getCell(i);
                    String val = cell == null ? null : getValue(cell);
                    try {
                        p.parse(vo, val);
                    } catch (Exception ex) {
                        hasErr.set(true);
                        err.append(ex.getMessage()).append("; ");
                    }
                });
                if (hasErr.get()) {
                    errSB.append(err).append("\n");
                } else {
                    try {
                        boolean continue_ = saveHandler.run(vo);
                        okRowsCount++;
                        if (!continue_) break;
                    } catch (Exception ex) {
                        errSB.append(err).append(ex.getMessage()).append("\n");
                        //ex.printStackTrace();
                    }
                }
            }

            String err = errSB.toString();
            if (err.length() > 0) throw new JDiyException(err);
            return okRowsCount;
        } else {
            throw new JDiyException("上传文件不正确。\n");
        }
    }


    public List<T> doImport(Map<String, HeadParser<T>> heads, int headRowNum, MultipartFile file1) throws JDiyException {
        if (!file1.isEmpty()) {
            Workbook book;
            try {
                if (file1.getOriginalFilename() != null && file1.getOriginalFilename().toLowerCase().endsWith(".xlsx")) {
                    book = new XSSFWorkbook(file1.getInputStream());
                } else {
                    book = new HSSFWorkbook(file1.getInputStream());
                }
            } catch (IOException e) {
                throw new JDiyException("无法读取Excel文件，请检查上传文件的有效性." + e.getMessage());
            }
            Sheet sheet = book.getSheetAt(0);

            StringBuilder errSB = new StringBuilder();
            int okRowsCount = 0;
            int breakNummRowCount = 0;//有时候excel编辑后，删除了一些行后保存导入，会出现提示很大的行号为null报错导致导入失败的bug,加这个参数，判断中间连续空行超过50行时停止导入
            int startRow = headRowNum - 1;
            //由于新版导入程序不再限定列的前后顺序(即用户导入时表头列的前后顺序可以打乱)
            //通过读取实际的表头行来确定目标列到底是什么字段，这个parserMap的key是列下标，value就是那个headParser.
            // 这样在读取数据行的每列数据的时候(列下标已知),就能通过此map确定目标列的处理函数。
            Map<Short, HeadParser<T>> parserMap = new HashMap<>();
            List<T> list = new ArrayList<>();
            for (int rowNum = startRow; rowNum <= sheet.getLastRowNum(); rowNum++) {
                AtomicBoolean hasErr = new AtomicBoolean(false);
                StringBuilder err = new StringBuilder("行" + (rowNum + 1) + ": ");

                Row row = sheet.getRow(rowNum);
                if (row == null) {
                    breakNummRowCount++;
                    if (breakNummRowCount > 50) break;
                    continue;
                } else {
                    breakNummRowCount = 0;
                }
                if (rowNum == startRow) {
                    for (short colNum = 0; colNum <= row.getLastCellNum(); colNum++) {//todo 测试下标
                        Cell cell = row.getCell(colNum);
                        String rn = getValue(cell);
                        if (heads.containsKey(rn)) {
                            parserMap.put(colNum, heads.get(rn));
                        }
                    }
                    continue;
                }

                T vo;
                try {
                    vo = tClass.getDeclaredConstructor().newInstance();
                } catch (Exception e) {
                    JDiyException le = new JDiyException(e.getMessage());
                    le.setStackTrace(e.getStackTrace());
                    throw le;
                }
                parserMap.forEach((i, p) -> {
                    Cell cell = row.getCell(i);
                    String val = cell == null ? null : getValue(cell);
                    try {
                        p.parse(vo, val);
                    } catch (Exception ex) {
                        hasErr.set(true);
                        err.append(ex.getMessage()).append("; ");
                    }
                });
                if (hasErr.get()) {
                    errSB.append(err).append("\n");
                } else {
                    try {
                        list.add(vo);
                    } catch (Exception ex) {
                        errSB.append(err).append(ex.getMessage()).append("\n");
                        //ex.printStackTrace();
                    }
                }
            }

            String err = errSB.toString();
            if (err.length() > 0) throw new JDiyException(err);
            return list;
        } else {
            throw new JDiyException("上传文件不正确。\n");
        }
    }

    /**
     * 得到Excel表中的值
     *
     * @param cell Excel中的每一个格子
     * @return Excel中每一个格子中的值
     */
    protected static String getValue(Cell cell) {
        if (cell == null) return null;
        if (cell.getCellType() == CellType.BOOLEAN) {
            // 返回布尔类型的值
            return String.valueOf(cell.getBooleanCellValue());
        } else if (cell.getCellType() == CellType.NUMERIC) {
            if (DateUtil.isCellDateFormatted(cell)) {
                double d = cell.getNumericCellValue();
                String s = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(DateUtil.getJavaDate(d));
                if (s.endsWith(" 00:00:00")) s = s.substring(0, s.length() - 9); //时间没有，去掉时间
                return s;
            } else {
                // 返回数值类型的值
                Number nm = cell.getNumericCellValue();
                if (nm.longValue() == nm.doubleValue()) return String.valueOf(nm.longValue());
                else {
                    return String.valueOf(nm.doubleValue());
                }
            }
        } else if (cell.getCellType() == CellType.FORMULA) {
            String strCell;
            try {
                strCell = String.valueOf(cell.getStringCellValue());
            } catch (IllegalStateException e) {
                strCell = String.valueOf(cell.getNumericCellValue());
            }
            return strCell;
        } else {
            // 返回字符串类型的值
            String s = String.valueOf(cell.getStringCellValue());
            return s == null ? null : s.trim();
        }
    }

}
